iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
Vue.js

Vue3歡樂套件箱耶系列 第 26

開箱26:Vue 3 + Firebase Storage存儲服務簡單實作

  • 分享至 

  • xImage
  •  

本次要跟姊姊一起學習Vue 3 + Firebase Storage 上傳檔案功能


▶ 如果您尚未建立專案/安裝 Firebase JS SDK 並初始化 Firebase,請先到
開箱23:Vue 3 + 建立web應用程式+Firebase JS SDK 初始化


官方文件:

簡介:Cloud Storage for Firebase
如何開始?Get started with Cloud Storage on Web

Firebase Storage 是 Google Cloud Platform 的一部分,專為 Firebase 開發者設計的雲端存儲解決方案。它允許開發者存儲和分享使用者生成的內容,例如圖片、音訊和影片。

主要特點:

強大的文件上傳/下載:不論網絡狀況如何,Firebase Storage 都可以確保文件的上傳和下載。即使在不穩定或差的網路環境下也能保持穩定。

安全性:Firebase Storage 配合 Firebase 的安全規則,讓你能夠精確地控制誰有存取、讀取、寫入的權限。

整合性:與 Firebase 的其他服務(如 Firebase Realtime Database、Firebase Authentication)有很好的整合,這使得開發流程更加順暢。

基於 Google Cloud Storage:Firebase Storage 在幕後實際上是由 Google Cloud Storage 支持的,這意味著它非常可靠,並且可以在必要時擴展到 Google Cloud 的其他功能和服務。

SDK 支援:Firebase Storage 提供 Android、iOS 和 JavaScript 的 SDK,讓開發者能夠輕鬆整合存儲功能到他們的應用程式或網站上。

性能和效率:自動適應網絡品質,如果應用程式在上傳或下載過程中發生斷網,Firebase Storage 可以在網絡重新連接時從斷點繼續。

使用 Firebase Storage 可以減少許多開發和維護雲端存儲解決方案的複雜性,並允許開發者專注於應用程式的核心功能。

免費使用的話:
存儲空間: 5GB 的總存儲容量。
下載流量: 每天 1GB 的下載數據。
上傳/下載操作: 上傳操作20萬次/日,下載操作5萬次/日。

事前準備:建立專案+安裝 Firebase JS SDK 並初始化 Firebase(若已建,可略過此步驟)

請先到
開箱23:Vue 3 + 建立web應用程式+Firebase JS SDK 初始化

開始實作本次功能吧!
https://ithelp.ithome.com.tw/upload/images/20231011/20142016Uzn0zo3VhM.png
▲ 成果

Demo網址:https://hahasister-ironman-project.netlify.app/#/uploadToFirebase
版本 "firebase": "^10.4.0"

☆★☆★ 詳細程式碼 前往 >> 本次程式 commit 紀錄

功能大綱
-實作可上傳圖片/取得上傳圖片清單/刪除圖片

結構
project-root/
├─ src/
│ ├─ views/
│ │ ├─ UploadToFirebase.vue // 新增操作頁面
│ │
│ ├─ services/
│ │ ├─ firebase.js // 引入
│ │
│ │
│ ├─ App.vue
│ ├─ main.js
└─ ...

步驟1:開啟「Firebase Storage」功能

回到Firebase console 控制台,開啟
https://ithelp.ithome.com.tw/upload/images/20231011/20142016ndQ48IbFko.png

可先以測試模式啟動(開通30天內-11/10,大家都可以讀寫資料庫),可之後再修改權限

Firebase 雲端儲存安全規則中的使用條件
安全規則保護 Cloud Storage 資料的訊息

https://ithelp.ithome.com.tw/upload/images/20231011/201420167Df8D3vSs5.png

選擇預設 Cloud Storage 儲存分區的位置。
https://ithelp.ithome.com.tw/upload/images/20231011/201420163NrIWCPtrM.png

步驟2:引入firebase/storage

修改 src/services/firebase.js

import { initializeApp } from "firebase/app";
import { getStorage } from 'firebase/storage';  // 新增

const firebaseConfig = {
  // ...
  storageBucket: ''
};

// Initialize Firebase
export const setupFirebase = initializeApp(firebaseConfig);

// Initialize Cloud Storage and get a reference to the service
export const storage = getStorage(setupFirebase); // 新增

步驟3:建立/上傳照片(基本範例)

創建參考:

詳細可看官方文件:Create a Reference

import { getStorage, ref } from "firebase/storage";

const storage = getStorage();

// Create a child reference
const imagesRef = ref(storage, 'images');
// imagesRef now points to 'images'

// Child references can also take paths delimited by '/'
const spaceRef = ref(storage, 'images/space.jpg');
// spaceRef now points to "images/space.jpg"
// imagesRef still points to "images"

以上是兩種不同創建方式的差異
https://ithelp.ithome.com.tw/upload/images/20231011/20142016Iq3boQT2MH.png

上傳文件:
多種寫法,本範例使用從Blob或File上傳呼叫uploadBytes()方法

詳細可看官方文件:Upload Files

整合起來,來完成一個基本範例如下:

功能大綱
上傳圖片 > 按下上傳 > 上傳至firebase

重點
因為有用到兩個ref,是會error的,所以將from 'firebase/storage'的ref重新命名為storageRef

<template>
  <div>
    <input type="file" accept="image/*" @change="handleFileSelect" />
    <button @click="upload">upload</button>
  </div>
</template>
<script setup>
import { storage } from '@/services/firebase.js';
import { ref as storageRef, uploadBytes } from 'firebase/storage';
import { ref } from 'vue';

const selectedFile = ref();

const handleFileSelect = event => {
  selectedFile.value = event.target.files[0];
};

const upload = () => {
  if (!selectedFile.value) {
    return;
  }
  const storageName = storageRef(storage, `images/${selectedFile.value.name}`);

  uploadBytes(storageName, selectedFile.value).then(snapshot => {
    console.log('Uploaded a blob or file!'); //上傳成功
  });
};
</script>

這樣就完成了基本成果拉!(下圖)
https://ithelp.ithome.com.tw/upload/images/20231011/201420162hTgsVklbl.png

並且到後台看一下是否新增成功
https://ithelp.ithome.com.tw/upload/images/20231011/20142016pVfc3lYP67.png

進階範例

** 前期提要 **
模板 /
上傳區塊:

  • 有一個文件輸入框 (file input),讓用戶選擇要上傳的圖片
  • 當檔案選擇有變動 (@change) 會調用 handleFileSelect 方法
  • 旁邊有一個 "上傳" 按鈕,點擊時會調用 upload 方法

圖片預覽區塊:

  • 若正在上傳,會顯示上傳進度條
  • 上傳完後,顯示上傳的圖片
  • 若無圖片,顯示 "圖片顯示於此" 文字

圖片清單區塊:

  • 列出所有已上傳的圖片(downloadURLs)
  • 每個圖片旁邊有一個刪除按鈕,點擊時會調用 removeImg 方法

資料宣告/
selectedFile:用戶選擇的圖片檔
fileInput:檔案輸入框的參考
downloadURL:上傳圖片後的下載連結
uploadProgress:上傳的進度
loading:是否正在載入圖片
downloadURLs:所有已上傳圖片的下載連結清單

方法宣告/
handleFileSelect:當用戶選擇圖片時檢查圖片大小,若超過2MB則警告
upload:上傳 selectedFile 到 Firebase Storage,並顯示上傳進度
getImgListAll:取得所有已上傳圖片的清單
removeImg:刪除指定的圖片。
onMounted:在組件掛載後,調用 getImgListAll 方法,取得所有已上傳的圖片清單

步驟3:建立/上傳照片+取得照片連結秀到畫面上(進階範例)

接著我們修改以上範例,上傳照片時要監控他的上傳進度...等等,可以改用以下方法

<template>
  <div>
    <p>上傳圖片</p>
    <div>
      <input type="file" ref="fileInput" accept="image/*" @change="handleFileSelect"/>
      <button type="button" @click="upload">上傳</button>
    </div>

    <div>
      <div v-if="uploadProgress !== null">
        <progress :value="uploadProgress" max="100">{{ uploadProgress }}%
        </progress>{{ uploadProgress }}%
      </div>
      <p v-if="downloadURL">
        <img :src="downloadURL" />
      </p>
      <p v-else >圖片顯示於此</p>
    </div>
  </div>

</template>
<script setup>
import { storage } from '@/services/firebase.js';
import {
  ref as storageRef,
  uploadBytesResumable,
  getDownloadURL,
} from 'firebase/storage';
import { onMounted, ref, nextTick } from 'vue';

const selectedFile = ref(); 
const fileInput = ref(null); 
const downloadURL = ref(); // 圖片產生的url
const uploadProgress = ref(null); // 進度條

const handleFileSelect = event => {
  const file = event.target.files[0]; // 抓取file

  if (file && file.size <= 2 * 1024 * 1024) { // 怕大家傳太大檔案,所以我自己限制
    selectedFile.value = file;
    downloadURL.value = '';
  } else {
    alert('文件大小超過2MB,請重新上傳');
    fileInput.value.value = ''; // 清空已上傳的檔案值
    selectedFile.value = ''; // 清空選擇的檔案
  }
};

const upload = () => {
  if (!selectedFile.value) {
    return;
  }
  
  const storageName = storageRef(storage, `images/${selectedFile.value.name}`);

  const uploadTask = uploadBytesResumable(storageName, selectedFile.value);

  uploadTask.on(
    'state_changed',
    snapshot => {
      const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      console.log('Upload is ' + progress + '% done');
      console.log('Upload state ', snapshot.state);
      uploadProgress.value = parseInt(progress); // 將進度以整數寫入
    },
    error => {
      console.log('Upload error', error);
    },
    async () => {
      downloadURL.value = await getDownloadURL(uploadTask.snapshot.ref); //取得圖片url
      console.log('File available at', downloadURL.value);
      uploadProgress.value = null; // 進度條清空
      fileInput.value.value = ''; // 清空input
      selectedFile.value = ''; // 清空input
    }
  );
};
</script>

步驟4:取得已上傳照片清單

<template>
    ...
    ...
  <div>
    <p>照片清單</p>
    <p v-if="loading">Loadig...</p>
    <div v-else v-for="item in downloadURLs">
      <img :src="item.url" alt="" :key="item.url"/>

      <button type="button" @click="removeImg(item.fullPath)">
        <img src="/src/assets/icon/remove.png" width="32" height="32" />
      </button>
    </div>
  </div>
  </template>

希望組出的清單陣列為
downloadURLs = [{url:...,fullPath:...},{url:...,fullPath:...}]

const getImgListAll = async () => {
  loading.value = true;
  await nextTick(); 
  downloadURLs.value = []; // 清單陣列
  const listRef = storageRef(storage, 'images'); // 取得資料夾為images的

  try {
    const res = await listAll(listRef); // 取得清單
    const urls = await Promise.all(
      res.items.map(async itemRef => ({
        url: await getDownloadURL(itemRef), // 將連結放入陣列
        fullPath: itemRef.fullPath 
      }))
    );
    downloadURLs.value = urls;
  } catch (error) {
    console.error('Error getting download URLs', error);
  } finally {
    loading.value = false;
  }
};


onMounted(() => {
  getImgListAll(); //畫面載入取得清單
});

步驟5:刪除已上傳照片

const removeImg = async fullPath => {
  try {
    await deleteObject(storageRef(storage, fullPath));
    await getImgListAll(); // 重新取得清單
  } catch (error) {
    console.error('Error deleting image', error);
  }
};

程式碼有點長,好難拆解步驟,詳細請前往 >> 本次程式 commit 紀錄

以上程式碼僅供簡單參考,尚有優化地方,例如:處理錯誤處理,即時取得清單,程式碼優化...等

那我們明天再見了~

/images/emoticon/emoticon06.gif


上一篇
開箱25:Vue 3 + Firebase Cloud Firestore 簡單CRUD功能
下一篇
開箱27:Vue 3 + Firebase Cloud Messaging 建立測試通知
系列文
Vue3歡樂套件箱耶30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言